require "drb"
require "open4"
require "timeout"

module TclBot
  class RemoteInterp
    WORKER_SCRIPT = File.join(BOT_ROOT, "script", "worker")

    class Error < ::StandardError; end
    class WorkerNotStartedError < Error; end
    class WorkerTimeoutError < Error; end
    
    class DRbInterpObject < ::DRbObject
      undef_method :proc
    end

    def initialize(filename)
      @filename = filename
      @state = IO.read(filename)
      @exposed_procs = []
    end
    
    def start
      unless @pid
        Timeout.timeout(5) do
          spawn_remote_interpreter_process
          retrieve_remote_interpreter
        end
      end
    rescue Timeout::Error => e
      stop
      raise WorkerNotStartedError, "worker process could not be started"
    end
    
    def stop
      if @pid
        kill_remote_interpreter_process
        @interp = nil
      end
    end
    
    def delegate=(delegate)
      @delegate = delegate
      @delegate.extend(DRbUndumped)
      interp.delegate = @delegate if interp
    end
    
    def expose(name)
      @exposed_procs << name
      interp.expose(name) if interp
    end  
    
    def eval(script, options = {})
      timeout = options.delete(:timeout) || 5
      limit   = options.delete(:limit)
      result  = nil

      start
      Timeout.timeout(timeout) do
        result = if limit
          interp.eval_and_truncate(script, limit)
        else
          interp.eval(script)
        end
      end
      @state = interp.to_tcl
      result
      
    rescue Timeout::Error => e
      stop
      raise WorkerTimeoutError, "time limit exceeded"
    end
    
    def method_missing(method, *args, &block)
      start
      interp.send(method, *args, &block)
    end
    
    protected
      attr_reader :interp, :delegate
      
      def spawn_remote_interpreter_process
        @pid, @stdin, @stdout, @stderr = Open4.popen4(WORKER_SCRIPT)
        @uri = @stderr.gets.chomp
      end
      
      def kill_remote_interpreter_process
        Process.kill("KILL", @pid)
        Process.wait(@pid)
        @service.stop_service
        @pid = @stdin = @stdout = @stderr = @uri = @service = nil
      end

      def retrieve_remote_interpreter
        @service = DRb.start_service_on_any_port
        initialize_interpreter
      end
      
      def initialize_interpreter
        @interp = DRbInterpObject.new(nil, @uri)
        @interp.delegate = delegate || self
        @exposed_procs.each { |name| @interp.expose(name) }
        @interp.eval(@state)
      end
  end
end
